<?php
/*
 * Project		: Adria IRC Bot realised with PHP 5.x
 * File			: ./class_irc.php
 * Version		: 0.1
 * Description	: Definition of IRC comunication interface. 
 * 				  This class is used for comunication with IRC server
 * 				  and keeping information about current connection.
 */

// Load IRC contstants 
require_once('class_irc_definitions.php');

define('IRC_Reply_Channel', 1);
define('IRC_Reply_Query',	2);
define('IRC_Reply_Notice',	3);

class IRC {
	public $Socket = FALSE;
	private $Server = '';
	private $MyHostName = '';
	private $Nick = '';
	private $LastLine = '';
	private $LastLineTime = 0;
	private $Channels = array();
	private $Reply_path ='';
	
	/*
	 * Sets private attributes of class. 
	 * This function _isn't_ designed for using in private modules. Use clients method like SetNick, Join etc.
	 */
	public function SetAttrib($AttribName, $Value)
	{
		if (isset($this->$AttribName)) return $this->$AttribName=$Value;
		else return false;
	}
	/*
	 * Unsets private attributes of class. 
	 * This function _isn't_ designed for using in private modules. Use clients method like SetNick, Join etc.
	 */
	public function UnSetAttrib($AttribName)
	{
		 unset($this->$AttribName);
	}
	
	public function GetLastLine()
	{
		return $this->LastLine;
	}
	
	public function GetLastLineTime()
	{
		return $this->LastLineTime;
	}
	
	public function EOF()
	{
		return feof($this->Socket);
	}
	public function IsConnected()
	{
		return ($this->Socket!=FALSE);
	}
	
	public function Connect($URL,$Port,&$ErrNo = NULL, &$ErrStr = NULL, $Timeout='n/a')
	{
		if($Timeout=='n/a') $this->Socket = @fsockopen($URL,$Port,$ErrNo,$ErrStr);
		else $this->Socket = @fsockopen($URL,$Port,$ErrNo,$ErrStr,$Timeout); 
		if (!$this->Socket) return false;
		else 
		{
		  	stream_set_timeout($this->Socket, 1);  // sets blocking timeout to 1 second
			return true;	
		}
	}
	public function Close()
	{
	  fclose($this->Socket);
	  $this->Socket = FALSE;
	}
	/*
	 * On SecureWrite=true, is $Text tested for posible "danger" commands like JOIN, NICK etc.
	 * if those commands are detected, ScWrite run relevant IRC class method.
	 */
	public function ScWrite($Text,$SecureWrite=true)
	{	
	  if($Text!='') 
	  {
		if ($SecureWrite)
		{
			switch (true)
			{
				case(eregi('NICK (.*)',$Text,$tmp)):
					$this->SetNick($tmp[1]);
					break;
					
				case(eregi('JOIN (.*)',$Text,$tmp)):
					$this->Join($tmp[1]);
					break;
					
				default: 
					$GLOBALS['Log']->Out($Text);
					return fwrite($this->Socket,($Text."\n\r"));
								
			}
			
		}
		else 
		{
			$GLOBALS['Log']->Out($Text);
			return fwrite($this->Socket,($Text."\n\r"));   // non secured socket write 
		}
	  }
	  else return false;
	}
	/*
  	 *  Returns TRUE if $mode is Channel mode (k,m,l ...) or false
  	 */
  	private function IsChannelMode($mode)
  	{
  		return in_array($mode,array('n','t','i','m','p','s','l','k'));
  	}
  	/*
  	 *  Returns TRUE if $mode is User mode (k,m,l ...) or false
  	 */
  	private function IsUserMode($mode)
  	{
  		return in_array($mode,array('o','v'));
  	}
  	/*
  	 *  Read one (1) of IRC line and save it into $this->LastLine
  	 *  
  	 * 	Other functions: 	Ping / pong reply; 
  	 * 						Hostame, Reply path Modes and other parsers - for internal use in IRC class 
  	 */
	public function ScReadLine()
  	{
      	$this->LastLine = trim(fgets($this->Socket,512));
      	if($this->LastLine!='') 
      	{
      		$GLOBALS['Log']->In($this->LastLine);
      	
      		// Automatic Ping/Pong reply
      		if (eregi("^PING :(.*)",$this->LastLine,$tmp)) $this->ScWrite('PONG '.$tmp[1],false);
      	
        	switch (true)
      		{
      		case eregi('^:[^ ]+ '.RPL_CHANNELMODEIS.' [^ ]+ ([^ ]+) :?([^ ]+)[ ]?(.*)',$this->LastLine,$tmp):
      		case eregi('^:[^ ]+ MODE ([^ ]+) ([^ ]+)[ ]?(.*)',$this->LastLine,$tmp):
      					$tmp[1]=strtolower($tmp[1]);$tmp[2]=strtolower($tmp[2]);$tmp[3]=strtolower($tmp[3]);
      					if (($tmp[3]!='')) $params=explode(' ',trim($tmp[3]));
						$i=0;
						$j=0;		// number of mode (vithout + and -)
						$flag='+';
						$flag_free=str_replace(array('+','-'),array('',''),$tmp[2]);
 						while((isset($tmp[2]{$i})) and ($tmp[2]{$i}!=' '))
						{
						    $chr = $tmp[2]{$i};
							if (($chr == '+') or ($chr == '-')) $flag = $chr;
							else
							{
								$j++;
								if($this->IsChannelMode($chr)) 
								{
									if ($flag=='+') {
										$par_pos=count($params)-(strlen($flag_free)-$j)-1;
										if ($par_pos>0) $this->Channels[$tmp[1]]['mode'][$chr]['param']=$params[$par_pos];
										else $this->Channels[$tmp[1]]['mode'][$chr]=array(); 
									}
									else unset($this->Channels[$tmp[1]]['mode'][$chr]);
								}
								elseif ($this->IsUserMode($chr))
								{
									$par_pos=count($params)-(strlen($flag_free)-$j)-1;
									if ($flag=='+') $this->Channels[$tmp[1]]['users'][$params[$par_pos]]['flags'][]=$chr;
									else{
										 foreach($this->Channels[$tmp[1]]['users'][$params[$par_pos]]['flags'] as $key=>$value)
										 	if ($value==$chr) unset($this->Channels[$tmp[1]]['users'][$params[$par_pos]]['flags'][$key]); 
										}
										unset($this->Channels[$tmp[1]]['mode'][$chr]);
								}
								else $GLOBALS['Log']->Sys('Unknown mode: '.$chr.' In MODE message: '.$this->LastLine);
							}
							$i++;
						}
						break;
      					
      		case eregi('^:([^!]+)![^ ]+ JOIN ([^ ]+).*',$this->LastLine,$tmp):
      			        if(strtolower($tmp[1])==strtolower($this->Nick))
      			        {
      			        	$this->Channels[$tmp[2]]=array();
      			        	$GLOBALS['Log']->Sys('Joins into '.$tmp[2]);
      			        }
      			        else
      			        {
      			        	$tmp[1]=strtolower($tmp[1]);$tmp[2]=strtolower($tmp[2]);
      					 	$this->Channels[$tmp[2]]['users'][$tmp[1]]=array();
      						$GLOBALS['Log']->Sys('User '.$tmp[1].' joins into ',$tmp[2]);
      			        }
      				   	break;
      		case eregi('^:([^!]+)![^ ]+ QUIT.*',$this->LastLine,$tmp):
      					$tmp[1]=strtolower($tmp[1]);
      					foreach($this->Channels as &$chan)
      						if (isset($chan['users'][$tmp[1]])) unset($chan['users'][$tmp[1]]);
      					$GLOBALS['Log']->Sys('User '.$tmp[1].' quits');
      					break;
      		case eregi(':([^!]+)![^ ]+ NICK ([^ ]+)',$this->LastLine,$tmp):
      					if(strtolower($tmp[1])==strtolower($this->Nick))
      					{
      						$this->Nick=$tmp[2];
      						$GLOBALS['Log']->Sys('Changed nick to '.$tmp[2]);
      					}
      			        else
      			        {
							$tmp[1]=strtolower($tmp[1]);$tmp[2]=strtolower($tmp[2]);
      			        	foreach($this->Channels as &$chan)
							{
      						  if (isset($chan['users'][$tmp[1]]))
      						  {
      						  	$chan['users'][$tmp[2]]=$chan['users'][$tmp[1]];
      						  	unset($chan['users'][$tmp[1]]);
      						  }
							}
      						$GLOBALS['Log']->Sys('User '.$tmp[1].' parts '.$tmp[2]);
      			        }
      					break;
      		case eregi('^:([^!]+)![^ ]+ PART ([^ ]+).*',$this->LastLine,$tmp):
      					$tmp[1]=strtolower($tmp[1]);$tmp[2]=strtolower($tmp[2]);
      					if(strtolower($tmp[1])==strtolower($this->Nick))
      					{
      						unset($this->Channels[$tmp[2]]);
      						$GLOBALS['Log']->Sys('Parts '.$tmp[2]);
      					}
      			        else
      			        {
      					 	unset($this->Channels[$tmp[2]]['users'][$tmp[1]]);
      						$GLOBALS['Log']->Sys('User '.$tmp[1].' parts '.$tmp[2]);
      			        }
      					break;
      		case eregi('^:[^ ]+ KICK ([^ ]+) ([^ ]+).*',$this->LastLine,$tmp):
      					$tmp[1]=strtolower($tmp[1]);$tmp[2]=strtolower($tmp[2]);
      					if(strtolower($tmp[2])==strtolower($this->Nick))
      					{
      						unset($this->Channels[$tmp[1]]);
      						$GLOBALS['Log']->Sys('Kicked from '.$tmp[1]);
      					}
      			        else
      			        {
      					 	unset($this->Channels[$tmp[1]]['users'][$tmp[2]]);
      						$GLOBALS['Log']->Sys('User '.$tmp[2].' was kicked from '.$tmp[1]);
      			        }
      					break;
      		case eregi('^:[^ ]+ TOPIC ([^ ]+) :(.*)',$this->LastLine,$tmp):
      					$tmp[1]=strtolower($tmp[1]);
      					$this->Channels[$tmp[1]]['topic']=$tmp[2];
      					$GLOBALS['Log']->Sys('Topic '.$tmp[1].' changed to: '.$tmp[2]);
      					break;					
      		case eregi('^:[^ ]+ '.RPL_TOPIC.' [^ ]+ ([^ ]+) :(.*)',$this->LastLine,$tmp):
      					$tmp[1]=strtolower($tmp[1]);
      					$GLOBALS['Log']->Sys('Topic '.$tmp[1].': '.$tmp[2]);
      					$this->Channels[$tmp[1]]['topic']=$tmp[2];
      					break;
      		case eregi('^:[^ ]+ '.RPL_NOTOPIC.' [^ ]+ ([^ ]+).*',$this->LastLine,$tmp):
      				    $tmp[1]=strtolower($tmp[1]);
      					$this->Channels[$tmp[1]]['topic']='';
      					$GLOBALS['Log']->Sys('No topic set in: '.$tmp[1]);
      					break;		
      		case eregi('^:[^ ]+ '.RPL_NAMREPLY.' [^ ]+ (=|\\*|@]) ([^ ]+) :(.*)',$this->LastLine, $tmp):
      			        $tmp[1]=strtolower($tmp[1]);$tmp[2]=strtolower($tmp[2]);$tmp[3]=strtolower($tmp[3]);
      			  		if(!isset($this->Channels[($tmp[2])])) $this->Channels[($tmp[2])]=array();
      			  		$users=explode(' ',$tmp[3]);
      					$i=0;
      					while(isset($users[$i]))
      					{
      						if(eregi('^(@|\\+)([^ ]+)',$users[$i],$tmp2)) 
      						{
      						   if(!isset($this->Channels[($tmp[2])]['users'][($tmp2[2])])) $this->Channels[($tmp[2])]['users'][trim($tmp2[2])]=array();
      						   $this->Channels[($tmp[2])]['users'][trim($tmp2[2])]['flags']=array(($tmp2[1]=='@'?'o':'v'));
      						}
      						else if(!isset($this->Channels[($tmp[2])]['users'][($users[$i])])) $this->Channels[($tmp[2])]['users'][trim($users[$i])]=array();
      						
      						$i++;
      					}
      					break;
      		}
      	
      		// ------ hosmame parser --------
      		if (($this->HostName=='') && ($this->Nick!=''))
      		 if (eregi(':'.$this->Nick.'!([^ ]+)*.',$this->LastLine,$tmp))
      		 {
      			$this->HostName=$tmp[1];
      			$GLOBALS['Log']->Sys('Get hostname: '.$this->HostName);
      		 }	 
      		
      		// -------- reply parser ------------
      		if (eregi('^:([^!]+)![^ ]+ PRIVMSG ([^ ]+) :?.*',$this->LastLine,$tmp))
      		{
      			if (strtolower($tmp[2])==strtolower($this->Nick)) {/*query*/ $this->Reply_path='PRIVMSG '.$tmp[1].' :';}
      			else {/*channel*/ $this->Reply_path='PRIVMSG '.$tmp[2].' :';}
      		}
      		elseif (eregi('^:([^!]+)![^ ]+ NOTICE '.$this->Nick.' :?.*',$this->LastLine,$tmp)){/*notice*/ $this->Reply_path='NOTICE '.$tmp[1].' :';}

      		//-------- LastLineTime ------------
			$this->LastLineTime = time(); 
      	}      	
  		return $this->LastLine;
  	}
  	
  	/*
  	 *  Returns actual nick
  	 */
  	public function GetNick()
  	{
  		return $this->Nick;
  	}
  	/*
  	 *  Try to join into $Channel
  	 */
  	public function Join($Channel)
  	{
  		$this->ScWrite('JOIN '.$Channel, false);		
  		$regexps_fails=array(
						':[^ ]+ '.ERR_NEEDMOREPARAMS.'( [^ ]*)? JOIN .*',
						':[^ ]+ '.ERR_BANNEDFROMCHAN.'( [^ ]*)? '.$Channel.' .*',
						':[^ ]+ '.ERR_INVITEONLYCHAN.'( [^ ]*)? '.$Channel.' .*',
						':[^ ]+ '.ERR_BADCHANNELKEY.'( [^ ]*)? '.$Channel.' .*',
  						':[^ ]+ '.ERR_CHANNELISFULL.'( [^ ]*)? '.$Channel.' .*',
  						':[^ ]+ '.ERR_BADCHANMASK.'( [^ ]*)? '.$Channel.' .*',
  						':[^ ]+ '.ERR_NOSUCHCHANNEL.'( [^ ]*)? '.$Channel.' .*',
  						':[^ ]+ '.ERR_TOOMANYCHANNELS.'( [^ ]*)? '.$Channel.' .*'
					   );
  		$code_fails = "\$GLOBALS['Log']->Sys('Can\'t join to channel $Channel');
				 \$this->DestroyAllTriggers();";
  		// positive reply 
  		$regexp_join = ':'.$this->Nick.'![^ ]+ JOIN :?'.$Channel.'.*';
  		$code_join = "\$this->DestroyAllTriggers();";
  		$tmp=new CodeEvent();
		$tmp->AddTrigger($regexps_fails,$code_fails);
		$tmp->AddTrigger($regexp_join,$code_join);
		$GLOBALS['Bot']->NewRunningEvent($tmp);
		unset($tmp);
  	}
  	/*
  	 *  Part $Channel
  	 */
  	public function Part($Channel)
  	{
  		//:BluPix!~BluPix@ip-234-221.ji.cz PART #sg.cz :moje
		$this->ScWrite('PART '.$Channel, false);		
  	}
  	/*
  	 *  Try to change nick to $Nick
  	 */
  	public function SetNick($Nick)
  	{
  		// Try to change Nick 
		$this->ScWrite('NICK '.$Nick, false);
		$code_fails='$this->DestroyAllTriggers();';
		$regexps_fails=array(
						':[^ ]+ '.ERR_NONICKNAMEGIVEN.'.*',
						':[^ ]+ '.ERR_ERRONEUSNICKNAME.'( [^ ]*)? '.$Nick.'.*',
						':[^ ]+ '.ERR_NICKCOLLISION.'( [^ ]*)? '.$Nick.'.*',
						':[^ ]+ '.ERR_UNAVAILRESOURCE.'( [^ ]*)? '.$Nick.'.*',
						':[^ ]+ '.ERR_RESTRICTED.'.*',
						':[^ ]+ '.ERR_NICKNAMEINUSE.'( [^ ]*)? '.$Nick.'.*'
					   );
		$regexp_ok=':'.$this->Nick.'![^ ]+ NICK :?'.$Nick;
		$code_ok='$GLOBALS[\'IRC\']->SetAttrib(\'Nick\',\''.$Nick.'\');$this->DestroyAllTriggers();';
		$tmp=new CodeEvent(5);	// valid for 5 second 
		$tmp->AddTrigger($regexp_ok,$code_ok);
		$tmp->AddTrigger($regexps_fails,$code_fails);
		$GLOBALS['Bot']->NewRunningEvent($tmp);
  	}
  	/*
  	 *  Kick $nick from $channel with $message
  	 */
  	public function Kick($nick,$channel,$message=NULL)
  	{
  		$this->ScWrite('KICK '.$channel.' '.$nick.($message!=NULL?(' :'.$message):''),false);
  	}
  	/*
  	 *  Send mode message $mode to $channel with $param
  	 */
  	public function Mode($mode, $channel,$param)
  	{
  		$tmp='MODE '.$channel.' '.$mode;
  		$tmp.=($param!=''?' '.$param:'');
  		return $this->ScWrite($tmp);
  	}
  	/*
  	 *  Send op mode message with $nick and $channel 
  	 */
  	public function Op($nick, $channel)
  	{
  		return $this->Mode('+o',$channel,$nick);
  	}
	/*
  	 *  Send deop mode message with $nick and $channel 
  	 */
  	public function Deop($nick, $channel)
  	{
  		return $this->Mode('-o',$channel,$nick);
  	} 
	/*
  	 *  Send voice mode message with $nick and $channel 
  	 */
  	public function Voice($nick, $channel)
  	{
  		return $this->Mode('+v',$channel,$nick);
  	}	
	/*
  	 *  Send devoice mode message with $nick and $channel 
  	 */
  	public function Devoice($nick, $channel)
  	{
  		return $this->Mode('-v',$channel,$nick);
  	}
  	/*
  	 *  Send reply to user which send command
  	 */
  	public function Reply($Text,$Reply_path='n/a')
  	{
  		if ($Reply_path=='n/a') $Reply_path=$this->Reply_path;
  		$this->ScWrite($Reply_path.$Text,false);
  	}
  	/*
  	 *  Returns Reply_path for function Reply
  	 */
  	public function GetReplyPath()
  	{
  		return $this->Reply_path;
  	}
  	/*
  	 *  Returns type of Reply (IRC_Reply_Channel, IRC_Reply_Query, IRC_Reply_Notice) in $Reply_path or actual
  	 */
  	public function GetReplyType($Reply_path='n/a')
  	{
  		if($Reply_path=='n/a') $Reply_path=$this->Reply_path;
  		$tmp = explode(' ',$Reply_path);
 		if (strtolower($tmp[0])=='notice') return IRC_Reply_Notice;
 		elseif ((strtolower($tmp[0])=='privmsg') and ($tmp[1]{0}=='#')) return IRC_Reply_Channel;
 		elseif ((strtolower($tmp[0])=='privmsg') and ($tmp[1]{1}!='#')) return IRC_Reply_Query;	
  	}
  	/*
  	 *  Sends $Text to $Target by PRIVMSG
  	 */
  	public function Message($Target,$Text)
  	{
  		$this->ScWrite('PRIVMSG '.$Target.' :'.$Text,false);
  	}
	/*
  	 *  Sends NOTICE with $Text to $Target
  	 */
  	public function Notice($Target,$Text)
  	{
  		$this->ScWrite('NOTICE '.$Target.' '.$Text,false);
  	}
  	/*
  	 *  Sends ACTION with $Text to $Target
  	 */
  	public function Action($Target,$Text)
  	{
  		$this->Message($Target,chr(1).'ACTION '.$Text.chr(1));
  	}
  	// ------------------ User functions -----------------------
	/*
	 *  Return TRUE if $Nick is +o in $Channel (bot muss be joined in $Channel) or returns FALSE 
	 */
	public function IsOper($Nick,$Channel)
	{
		return in_array('o',$this->Channels[strtolower($Channel)]['users'][strtolower($Nick)]['flags']);
	}
	/*
	 *  Return TRUE if $Nick is +v in $Channel (bot muss be joined in $Channel) or returns FALSE 
	 */
	public function HasVoice($Nick,$Channel)
	{
		return in_array('v',$this->Channels[strtolower($Channel)]['users'][strtolower($Nick)]['flags']);
	}	
	/*
	 *  Return array of flags (+o,v..) for $Nick in $Channel (bot muss be joined in $Channel) or returns FALSE
	 */
	public function GetFlags($Nick,$Channel)
	{
		return $this->Channels[strtolower($Channel)]['users'][strtolower($Nick)]['flags'];
	}
	/*
	 *  Return TRUE if $Nick is in $Channel (bot muss be joined in $Channel) or returns FALSE 
	 */
	public function IsJoinedIn($Nick,$Channel)
	{
		return array_key_exists(strtolower($Nick),$this->Channels[strtolower($Channel)]['users']);
	}
	// ------------------- Channel function ---------------------
	/*
	 *  Return topic of $Channel (bot muss be joined in $Channel)
	 */
	public function GetTopic($Channel)
	{
		return $this->Channels[strtolower($Channel)]['topic'];
	}
	/*
	 *  Return if is bot in $Channel
	 */
	public function JoinedIn($Channel)
	{
		return isset($this->Channels[strtolower($Channel)]);
	}
	/*
	 *  Return array of $Channel modes (bot muss be joined in $Channel)
	 */
	public function GetChanMode($Channel)
	{
		return $this->Channels[strtolower($Channel)]['mode'];
	}
	/*
	 *  Return array with users joined in $Channel (bot muss be joined in $Channel)
	 */
	public function GetUsers($Channel)
	{
		$tmp=array();
		foreach($this->Channels[strtolower($Channel)]['users'] as $key=>$value)
		  	$tmp[]=$key;
	    return $tmp;
	}
}
?>